/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the EternalTech source code.

 The EternalTech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The EternalTech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the EternalTech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// r_render.cpp - rendering routines
//


#include "r_local.h"


/*
 ==============================================================================

 MATERIAL RENDERING SETUP

 ==============================================================================
*/


/*
 ==================
 RB_Cull
 ==================
*/
void RB_Cull (){

	if (backEnd.material->cullType == CT_TWO_SIDED)
		GL_Disable(GL_CULL_FACE);
	else {
		GL_Enable(GL_CULL_FACE);

		if (backEnd.material->cullType == CT_FRONT_SIDED)
			GL_CullFace(GL_FRONT);
		else
			GL_CullFace(GL_BACK);
	}
}

/*
 ==================
 RB_PolygonOffset
 ==================
*/
void RB_PolygonOffset (){

	if (backEnd.material->flags & MF_POLYGONOFFSET){
		GL_Enable(GL_POLYGON_OFFSET_FILL);
		GL_PolygonOffset(r_offsetFactor->floatValue, r_offsetUnits->floatValue * backEnd.material->polygonOffset);
	}
	else
		GL_Disable(GL_POLYGON_OFFSET_FILL);
}

/*
 ==================
 RB_DrawState
 ==================
*/
void RB_DrawState (stage_t *stage){

	if (stage->drawState & DS_POLYGONOFFSET){
		GL_Enable(GL_POLYGON_OFFSET_FILL);
		GL_PolygonOffset(r_offsetFactor->floatValue, r_offsetUnits->floatValue * stage->polygonOffset);
	}
	else
		GL_Disable(GL_POLYGON_OFFSET_FILL);

	if (stage->drawState & DS_BLEND){
		GL_Enable(GL_BLEND);
		GL_BlendFunc(stage->blendSrc, stage->blendDst);
		GL_BlendEquation(stage->blendMode);
	}
	else
		GL_Disable(GL_BLEND);

	if (stage->drawState & DS_ALPHATEST){
		GL_Enable(GL_ALPHA_TEST);
		GL_AlphaFunc(GL_GREATER, backEnd.registers[stage->alphaTestRegister]);
	}
	else
		GL_Disable(GL_ALPHA_TEST);

	if (stage->drawState & DS_IGNOREALPHATEST)
		GL_DepthFunc(GL_LEQUAL);
	else
		GL_DepthFunc(GL_EQUAL);

	GL_ColorMask(!(stage->drawState & DS_MASKRED), !(stage->drawState & DS_MASKGREEN), !(stage->drawState & DS_MASKBLUE), !(stage->drawState & DS_MASKALPHA));
}

/*
 ==================
 RB_BindTexture
 ==================
*/
void RB_BindTexture (texture_t *texture, cinHandle_t cinematicHandle){

	cinData_t	data;

	// If a cinematic
	if (cinematicHandle){
		if (r_skipVideos->integerValue){
			GL_BindTexture(rg.blackTexture);
			return;
		}

		// Decode a video frame
		data = CIN_UpdateCinematic(cinematicHandle, backEnd.time);

		// Bind the texture
		if (!data.image)
			GL_BindTexture(rg.blackTexture);
		else {
			GL_BindTexture(texture);

			// Update the texture if needed
			if (!data.dirty)
				return;

			R_UploadTextureImage(texture, 0, data.image, data.width, data.height);
		}

		return;
	}

	// Bind the texture
	GL_BindTexture(texture);

	// Capture the current color if needed
	if (texture == rg.currentColorTexture && (backEnd.material->flags & MF_UPDATECURRENTCOLOR)){
		backEnd.currentColorCaptured = backEnd.material->sort;

		R_CopyFramebufferToTexture(texture, 0, backEnd.viewport.X(), backEnd.viewport.Y(), backEnd.viewport.Width(), backEnd.viewport.Height());
	}

	// Capture the current depth if needed
	if (texture == rg.currentDepthTexture && (backEnd.material->flags & MF_UPDATECURRENTDEPTH)){
		backEnd.currentDepthCaptured = true;

		R_CopyFramebufferToTexture(texture, 0, backEnd.viewport.X(), backEnd.viewport.Y(), backEnd.viewport.Width(), backEnd.viewport.Height());
	}
}

/*
 ==================
 RB_BindMultitexture
 ==================
*/
void RB_BindMultitexture (texture_t *texture, cinHandle_t cinematicHandle, int unit){

	cinData_t	data;

	// If a cinematic
	if (cinematicHandle){
		if (r_skipVideos->integerValue){
			GL_BindMultitexture(rg.blackTexture, unit);
			return;
		}

		// Decode a video frame
		data = CIN_UpdateCinematic(cinematicHandle, backEnd.time);

		// Bind the texture
		if (!data.image)
			GL_BindMultitexture(rg.blackTexture, unit);
		else {
			GL_BindMultitexture(texture, unit);

			// Update the texture if needed
			if (!data.dirty)
				return;

			R_UploadTextureImage(texture, unit, data.image, data.width, data.height);
		}

		return;
	}

	// Bind the texture
	GL_BindMultitexture(texture, unit);

	// Capture the current color if needed
	if (texture == rg.currentColorTexture && (backEnd.material->flags & MF_UPDATECURRENTCOLOR)){
		backEnd.currentColorCaptured = backEnd.material->sort;

		R_CopyFramebufferToTexture(texture, unit, backEnd.viewport.X(), backEnd.viewport.Y(), backEnd.viewport.Width(), backEnd.viewport.Height());
	}

	// Capture the current depth if needed
	if (texture == rg.currentDepthTexture && (backEnd.material->flags & MF_UPDATECURRENTDEPTH)){
		backEnd.currentDepthCaptured = true;

		R_CopyFramebufferToTexture(texture, unit, backEnd.viewport.X(), backEnd.viewport.Y(), backEnd.viewport.Width(), backEnd.viewport.Height());
	}
}

/*
 ==================
 RB_ComputeTexturePlanes
 ==================
*/
void RB_ComputeTexturePlanes (textureStage_t *textureStage, const float *registers, glqVec4 planes[4]){

	glqMat4	matrix;

	if (textureStage->texGen == TG_EXPLICIT || textureStage->texGen == TG_NORMAL || textureStage->texGen == TG_REFLECT)
		return;

	if (textureStage->texGen == TG_VECTOR){
		planes[0] = textureStage->texGenVectors[0];
		planes[1] = textureStage->texGenVectors[1];

		return;
	}

	if (textureStage->texGen == TG_SKYBOX){
		matrix.Identity();
		matrix.Translate(-backEnd.localParms.viewOrigin[0], -backEnd.localParms.viewOrigin[1], -backEnd.localParms.viewOrigin[2]);
	}

	if (textureStage->texGen == TG_WOBBLESKY){
		matrix.Identity();
		matrix.Rotate(-registers[textureStage->texGenRegisters[0]], 1.0f, 0.0f, 0.0f);
		matrix.Rotate(-registers[textureStage->texGenRegisters[1]], 0.0f, 1.0f, 0.0f);
		matrix.Rotate(-registers[textureStage->texGenRegisters[2]], 0.0f, 0.0f, 1.0f);
		matrix.Translate(-backEnd.localParms.viewOrigin[0], -backEnd.localParms.viewOrigin[1], -backEnd.localParms.viewOrigin[2]);
	}

	if (textureStage->texGen == TG_SCREEN){
		if (backEnd.entity->e.model == NULL)
			matrix = backEnd.viewParms.modelviewProjectionMatrix;
		else
			matrix = backEnd.viewParms.projectionMatrix * backEnd.localParms.viewMatrix;

		if (backEnd.material->subviewType == ST_MIRROR){
			matrix[0][0] = -matrix[0][0];
			matrix[1][0] = -matrix[1][0];
			matrix[2][0] = -matrix[2][0];
			matrix[3][0] = -matrix[3][0];
		}
	}

	planes[0].Set(matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0]);
	planes[1].Set(matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1]);
	planes[2].Set(matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2]);
	planes[3].Set(matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3]);
}

/*
 ==================
 RB_ComputeTextureMatrix
 ==================
*/
void RB_ComputeTextureMatrix (textureStage_t *textureStage, const float *registers, glqMat4 &matrix){

	float	s, t, angle;
	int		i;

	matrix.Identity();

	for (i = 0; i < textureStage->numTexMods; i++){
		switch (textureStage->texMods[i]){
		case TM_TRANSLATE:
			s = registers[textureStage->texModsRegisters[i][0]];
			t = registers[textureStage->texModsRegisters[i][1]];

			matrix.Translate(s, t, 0.0f);

			break;
		case TM_SCALE:
			s = registers[textureStage->texModsRegisters[i][0]];
			t = registers[textureStage->texModsRegisters[i][1]];

			matrix.Scale(s, t, 1.0f);

			break;
		case TM_CENTERSCALE:
			s = registers[textureStage->texModsRegisters[i][0]];
			t = registers[textureStage->texModsRegisters[i][1]];

			matrix.Translate(0.5f, 0.5f, 0.0f);
			matrix.Scale(s, t, 1.0f);
			matrix.Translate(-0.5f, -0.5f, 0.0f);

			break;
		case TM_SHEAR:
			s = registers[textureStage->texModsRegisters[i][0]];
			t = registers[textureStage->texModsRegisters[i][1]];

			matrix.Translate(0.5f, 0.5f, 0.0f);
			matrix.Shear(s, t, 1.0f);
			matrix.Translate(-0.5f, -0.5f, 0.0f);

			break;
		case TM_ROTATE:
			angle = registers[textureStage->texModsRegisters[i][0]];

			matrix.Translate(0.5f, 0.5f, 0.0f);
			matrix.Rotate(-angle, 0.0f, 0.0f, 1.0f);
			matrix.Translate(-0.5f, -0.5f, 0.0f);

			break;
		}
	}
}

/*
 ==================
 RB_SetupTextureStage
 ==================
*/
void RB_SetupTextureStage (textureStage_t *textureStage){

	glqMat4	matrix;
	glqVec4	planes[4];

	// Enable texturing
	GL_EnableTexture(textureStage->texture->target);

	// Bind the texture
	RB_BindTexture(textureStage->texture, textureStage->cinematicHandle);

	// Set up the texture coords and matrix
	RB_ComputeTexturePlanes(textureStage, backEnd.registers, planes);

	switch (textureStage->texGen){
	case TG_EXPLICIT:
		qglEnableClientState(GL_TEXTURE_COORD_ARRAY);
		qglTexCoordPointer(2, GL_FLOAT, sizeof(glVertex_t), GL_VERTEX_TEXCOORD(backEnd.vertexPointer));

		if (!textureStage->numTexMods)
			GL_LoadIdentity(GL_TEXTURE);
		else {
			RB_ComputeTextureMatrix(textureStage, backEnd.registers, matrix);

			GL_LoadMatrix(GL_TEXTURE, matrix);
		}

		break;
	case TG_VECTOR:
		GL_Enable(GL_TEXTURE_GEN_S);
		GL_Enable(GL_TEXTURE_GEN_T);

		GL_TexGen(GL_S, GL_OBJECT_LINEAR);
		GL_TexGen(GL_T, GL_OBJECT_LINEAR);

		qglTexGenfv(GL_S, GL_OBJECT_PLANE, planes[0].ToFloatPtr());
		qglTexGenfv(GL_T, GL_OBJECT_PLANE, planes[1].ToFloatPtr());

		if (!textureStage->numTexMods)
			GL_LoadIdentity(GL_TEXTURE);
		else {
			RB_ComputeTextureMatrix(textureStage, backEnd.registers, matrix);

			GL_LoadMatrix(GL_TEXTURE, matrix);
		}

		break;
	case TG_NORMAL:
		qglEnableClientState(GL_NORMAL_ARRAY);
		qglNormalPointer(GL_FLOAT, sizeof(glVertex_t), GL_VERTEX_NORMAL(backEnd.vertexPointer));

		GL_Enable(GL_TEXTURE_GEN_S);
		GL_Enable(GL_TEXTURE_GEN_T);
		GL_Enable(GL_TEXTURE_GEN_R);

		GL_TexGen(GL_S, GL_NORMAL_MAP);
		GL_TexGen(GL_T, GL_NORMAL_MAP);
		GL_TexGen(GL_R, GL_NORMAL_MAP);

		GL_LoadTransposeMatrix(GL_TEXTURE, backEnd.viewParms.modelviewMatrix);

		break;
	case TG_REFLECT:
		qglEnableClientState(GL_NORMAL_ARRAY);
		qglNormalPointer(GL_FLOAT, sizeof(glVertex_t), GL_VERTEX_NORMAL(backEnd.vertexPointer));

		GL_Enable(GL_TEXTURE_GEN_S);
		GL_Enable(GL_TEXTURE_GEN_T);
		GL_Enable(GL_TEXTURE_GEN_R);

		GL_TexGen(GL_S, GL_REFLECTION_MAP);
		GL_TexGen(GL_T, GL_REFLECTION_MAP);
		GL_TexGen(GL_R, GL_REFLECTION_MAP);

		GL_LoadTransposeMatrix(GL_TEXTURE, backEnd.viewParms.modelviewMatrix);

		break;
	case TG_SKYBOX:
		GL_Enable(GL_TEXTURE_GEN_S);
		GL_Enable(GL_TEXTURE_GEN_T);
		GL_Enable(GL_TEXTURE_GEN_R);

		GL_TexGen(GL_S, GL_OBJECT_LINEAR);
		GL_TexGen(GL_T, GL_OBJECT_LINEAR);
		GL_TexGen(GL_R, GL_OBJECT_LINEAR);

		qglTexGenfv(GL_S, GL_OBJECT_PLANE, planes[0].ToFloatPtr());
		qglTexGenfv(GL_T, GL_OBJECT_PLANE, planes[1].ToFloatPtr());
		qglTexGenfv(GL_R, GL_OBJECT_PLANE, planes[2].ToFloatPtr());

		GL_LoadIdentity(GL_TEXTURE);

		break;
	case TG_WOBBLESKY:
		GL_Enable(GL_TEXTURE_GEN_S);
		GL_Enable(GL_TEXTURE_GEN_T);
		GL_Enable(GL_TEXTURE_GEN_R);

		GL_TexGen(GL_S, GL_OBJECT_LINEAR);
		GL_TexGen(GL_T, GL_OBJECT_LINEAR);
		GL_TexGen(GL_R, GL_OBJECT_LINEAR);

		qglTexGenfv(GL_S, GL_OBJECT_PLANE, planes[0].ToFloatPtr());
		qglTexGenfv(GL_T, GL_OBJECT_PLANE, planes[1].ToFloatPtr());
		qglTexGenfv(GL_R, GL_OBJECT_PLANE, planes[2].ToFloatPtr());

		GL_LoadIdentity(GL_TEXTURE);

		break;
	case TG_SCREEN:
		GL_Enable(GL_TEXTURE_GEN_S);
		GL_Enable(GL_TEXTURE_GEN_T);
		GL_Enable(GL_TEXTURE_GEN_Q);

		GL_TexGen(GL_S, GL_OBJECT_LINEAR);
		GL_TexGen(GL_T, GL_OBJECT_LINEAR);
		GL_TexGen(GL_Q, GL_OBJECT_LINEAR);

		qglTexGenfv(GL_S, GL_OBJECT_PLANE, planes[0].ToFloatPtr());
		qglTexGenfv(GL_T, GL_OBJECT_PLANE, planes[1].ToFloatPtr());
		qglTexGenfv(GL_Q, GL_OBJECT_PLANE, planes[3].ToFloatPtr());

		if (!textureStage->numTexMods)
			GL_LoadIdentity(GL_TEXTURE);
		else {
			RB_ComputeTextureMatrix(textureStage, backEnd.registers, matrix);

			GL_LoadMatrix(GL_TEXTURE, matrix);
		}

		break;
	}
}

/*
 ==================
 RB_CleanupTextureStage
 ==================
*/
void RB_CleanupTextureStage (textureStage_t *textureStage){

	if (textureStage->texGen == TG_EXPLICIT){
		qglDisableClientState(GL_TEXTURE_COORD_ARRAY);
		return;
	}

	if (textureStage->texGen == TG_NORMAL || textureStage->texGen == TG_REFLECT)
		qglDisableClientState(GL_NORMAL_ARRAY);

	GL_Disable(GL_TEXTURE_GEN_Q);
	GL_Disable(GL_TEXTURE_GEN_R);
	GL_Disable(GL_TEXTURE_GEN_T);
	GL_Disable(GL_TEXTURE_GEN_S);
}

/*
 ==================
 RB_SetupColorStage
 ==================
*/
void RB_SetupColorStage (colorStage_t *colorStage){

	glqVec4	color;

	// Set up the color
	if (backEnd.depthFilling){
		color[0] = 0.0f;
		color[1] = 0.0f;
		color[2] = 0.0f;
		color[3] = backEnd.registers[colorStage->registers[3]];
	}
	else {
		color[0] = backEnd.registers[colorStage->registers[0]];
		color[1] = backEnd.registers[colorStage->registers[1]];
		color[2] = backEnd.registers[colorStage->registers[2]];
		color[3] = backEnd.registers[colorStage->registers[3]];
	}

	// Set up the vertex color and texture environment
	switch (colorStage->vertexColor){
	case VC_IGNORE:
		if (!backEnd.depthFilling && colorStage->identity){
			GL_TexEnv(GL_REPLACE);
			break;
		}

		qglColor4fv(color.ToFloatPtr());

		GL_TexEnv(GL_MODULATE);

		break;
	case VC_MODULATE:
		qglEnableClientState(GL_COLOR_ARRAY);
		qglColorPointer(4, GL_UNSIGNED_BYTE, sizeof(glVertex_t), GL_VERTEX_COLOR(backEnd.vertexPointer));

		GL_TexEnv(GL_MODULATE);

		if (!backEnd.depthFilling && colorStage->identity)
			break;

		GL_SelectTexture(1);
		GL_EnableTexture(rg.whiteTexture->target);
		GL_BindTexture(rg.whiteTexture);

		GL_TexEnv(GL_COMBINE);

		qglTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PREVIOUS);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR);
		qglTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1);

		qglTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PREVIOUS);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_CONSTANT);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);
		qglTexEnvi(GL_TEXTURE_ENV, GL_ALPHA_SCALE, 1);

		qglTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color.ToFloatPtr());

		GL_SelectTexture(0);

		break;
	case VC_INVERSE_MODULATE:
		qglEnableClientState(GL_COLOR_ARRAY);
		qglColorPointer(4, GL_UNSIGNED_BYTE, sizeof(glVertex_t), GL_VERTEX_COLOR(backEnd.vertexPointer));

		GL_TexEnv(GL_COMBINE);

		qglTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PRIMARY_COLOR);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_ONE_MINUS_SRC_COLOR);
		qglTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1);

		qglTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_TEXTURE);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_PRIMARY_COLOR);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		qglTexEnvi(GL_TEXTURE_ENV, GL_ALPHA_SCALE, 1);

		if (!backEnd.depthFilling && colorStage->identity)
			break;

		GL_SelectTexture(1);
		GL_EnableTexture(rg.whiteTexture->target);
		GL_BindTexture(rg.whiteTexture);

		GL_TexEnv(GL_COMBINE);

		qglTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PREVIOUS);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR);
		qglTexEnvi(GL_TEXTURE_ENV, GL_RGB_SCALE, 1);

		qglTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PREVIOUS);
		qglTexEnvi(GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_CONSTANT);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA);
		qglTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);
		qglTexEnvi(GL_TEXTURE_ENV, GL_ALPHA_SCALE, 1);

		qglTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color.ToFloatPtr());

		GL_SelectTexture(0);

		break;
	}
}

/*
 ==================
 RB_CleanupColorStage
 ==================
*/
void RB_CleanupColorStage (colorStage_t *colorStage){

	if (colorStage->vertexColor == VC_IGNORE)
		return;

	qglDisableClientState(GL_COLOR_ARRAY);

	if (!backEnd.depthFilling && colorStage->identity)
		return;

	GL_SelectTexture(1);
	GL_DisableTexture();
	GL_SelectTexture(0);
}

/*
 ==================
 RB_SetupShaderStage
 ==================
*/
void RB_SetupShaderStage (shaderStage_t *shaderStage){

	uniform_t		*uniform;
	shaderParm_t	*shaderParm;
	shaderMap_t		*shaderMap;
	int				i;

	// Bind the program
	GL_BindProgram(shaderStage->program);

	// Set up the arrays
	if (shaderStage->program->vertexAttribs & VA_NORMAL){
		qglEnableVertexAttribArray(GL_ATTRIB_NORMAL);
		qglVertexAttribPointer(GL_ATTRIB_NORMAL, 3, GL_FLOAT, false, sizeof(glVertex_t), GL_VERTEX_NORMAL(backEnd.vertexPointer));
	}

	if (shaderStage->program->vertexAttribs & VA_TANGENT1){
		qglEnableVertexAttribArray(GL_ATTRIB_TANGENT1);
		qglVertexAttribPointer(GL_ATTRIB_TANGENT1, 3, GL_FLOAT, false, sizeof(glVertex_t), GL_VERTEX_TANGENT1(backEnd.vertexPointer));
	}

	if (shaderStage->program->vertexAttribs & VA_TANGENT2){
		qglEnableVertexAttribArray(GL_ATTRIB_TANGENT2);
		qglVertexAttribPointer(GL_ATTRIB_TANGENT2, 3, GL_FLOAT, false, sizeof(glVertex_t), GL_VERTEX_TANGENT2(backEnd.vertexPointer));
	}

	if (shaderStage->program->vertexAttribs & VA_TEXCOORD){
		qglEnableVertexAttribArray(GL_ATTRIB_TEXCOORD);
		qglVertexAttribPointer(GL_ATTRIB_TEXCOORD, 2, GL_FLOAT, false, sizeof(glVertex_t), GL_VERTEX_TEXCOORD(backEnd.vertexPointer));
	}

	if (shaderStage->program->vertexAttribs & VA_COLOR){
		qglEnableVertexAttribArray(GL_ATTRIB_COLOR);
		qglVertexAttribPointer(GL_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, true, sizeof(glVertex_t), GL_VERTEX_COLOR(backEnd.vertexPointer));
	}

	// Set up the predefined uniforms
	for (i = 0, uniform = shaderStage->program->uniforms; i < shaderStage->program->numUniforms; i++, uniform++){
		if (uniform->type == UT_CUSTOM)
			continue;

		switch (uniform->type){
		case UT_VIEW_ORIGIN:
			R_UniformVector3(uniform, backEnd.localParms.viewOrigin);
			break;
		case UT_VIEW_AXIS:
			R_UniformMatrix3(uniform, GL_TRUE, backEnd.localParms.viewAxis);
			break;
		case UT_ENTITY_ORIGIN:
			R_UniformVector3(uniform, backEnd.entity->e.origin);
			break;
		case UT_ENTITY_AXIS:
			R_UniformMatrix3(uniform, GL_TRUE, backEnd.entity->e.axis);
			break;
		case UT_SCREEN_MATRIX:
			R_UniformMatrix4(uniform, GL_TRUE, backEnd.viewParms.projectionMatrix * backEnd.localParms.viewMatrix);
			break;
		case UT_COORD_SCALE_AND_BIAS:
			R_UniformFloat4(uniform, backEnd.coordScale[0], backEnd.coordScale[1], backEnd.coordBias[0], backEnd.coordBias[1]);
			break;
		case UT_COLOR_SCALE_AND_BIAS:
			R_UniformFloat2(uniform, shaderStage->colorScale, shaderStage->colorBias);
			break;
		}
	}

	// Set up the shader parms
	for (i = 0, shaderParm = shaderStage->shaderParms; i < shaderStage->numShaderParms; i++, shaderParm++){
		switch (shaderParm->uniform->format){
		case GL_FLOAT:
			R_UniformFloat(shaderParm->uniform, backEnd.registers[shaderParm->registers[0]]);
			break;
		case GL_FLOAT_VEC2:
			R_UniformFloat2(shaderParm->uniform, backEnd.registers[shaderParm->registers[0]], backEnd.registers[shaderParm->registers[1]]);
			break;
		case GL_FLOAT_VEC3:
			R_UniformFloat3(shaderParm->uniform, backEnd.registers[shaderParm->registers[0]], backEnd.registers[shaderParm->registers[1]], backEnd.registers[shaderParm->registers[2]]);
			break;
		case GL_FLOAT_VEC4:
			R_UniformFloat4(shaderParm->uniform, backEnd.registers[shaderParm->registers[0]], backEnd.registers[shaderParm->registers[1]], backEnd.registers[shaderParm->registers[2]], backEnd.registers[shaderParm->registers[3]]);
			break;
		}
	}

	// Set up the shader maps
	for (i = 0, shaderMap = shaderStage->shaderMaps; i < shaderStage->numShaderMaps; i++, shaderMap++)
		RB_BindMultitexture(shaderMap->texture, shaderMap->cinematicHandle, shaderMap->uniform->unit);
}

/*
 ==================
 RB_CleanupShaderStage
 ==================
*/
void RB_CleanupShaderStage (shaderStage_t *shaderStage){

	GL_SelectTexture(0);

	if (shaderStage->program->vertexAttribs & VA_COLOR)
		qglDisableVertexAttribArray(GL_ATTRIB_COLOR);

	if (shaderStage->program->vertexAttribs & VA_TEXCOORD)
		qglDisableVertexAttribArray(GL_ATTRIB_TEXCOORD);

	if (shaderStage->program->vertexAttribs & VA_TANGENT2)
		qglDisableVertexAttribArray(GL_ATTRIB_TANGENT2);

	if (shaderStage->program->vertexAttribs & VA_TANGENT1)
		qglDisableVertexAttribArray(GL_ATTRIB_TANGENT1);

	if (shaderStage->program->vertexAttribs & VA_NORMAL)
		qglDisableVertexAttribArray(GL_ATTRIB_NORMAL);

	GL_BindProgram(NULL);
}


/*
 ==============================================================================

 RENDERING SETUP & UTILITIES

 ==============================================================================
*/


/*
 ==================
 RB_EntityState
 ==================
*/
void RB_EntityState (entity_t *entity){

	// Transform view origin and view axis into local space
	if (entity->e.model == NULL){
		backEnd.localParms.viewOrigin = backEnd.viewParms.origin;
		backEnd.localParms.viewAxis = backEnd.viewParms.axis;
	}
	else {
		R_WorldPointToLocal(backEnd.viewParms.origin, backEnd.localParms.viewOrigin, entity->e.origin, entity->e.axis);
		R_WorldAxisToLocal(backEnd.viewParms.axis, backEnd.localParms.viewAxis, entity->e.axis);
	}

	// Compute the view matrix
	if (entity->e.model == NULL)
		backEnd.localParms.viewMatrix = backEnd.viewParms.modelviewMatrix;
	else
		backEnd.localParms.viewMatrix = backEnd.viewParms.modelviewMatrix.Multiply(entity->e.axis, entity->e.origin);

	// Set the modelview matrix
	GL_LoadMatrix(GL_MODELVIEW, backEnd.localParms.viewMatrix);

	// Set the depth range
	if (entity->e.depthHack)
		GL_DepthRange(0.0f, 0.3f);
	else
		GL_DepthRange(0.0f, 1.0f);
}

/*
 ==================
 RB_EntityShadowState
 ==================
*/
void RB_EntityShadowState (entity_t *entity, light_t *light){

	// Transform view origin and view axis into local space
	if (entity->e.model == NULL){
		backEnd.localParms.viewOrigin = light->data.origin;
		backEnd.localParms.viewAxis = light->data.axis;
	}
	else {
		R_WorldPointToLocal(light->data.origin, backEnd.localParms.viewOrigin, entity->e.origin, entity->e.axis);
		R_WorldAxisToLocal(light->data.axis, backEnd.localParms.viewAxis, entity->e.axis);
	}

	// Compute the view matrix
	if (entity->e.model == NULL)
		backEnd.localParms.viewMatrix = backEnd.shadowMap.modelviewMatrix;
	else
		backEnd.localParms.viewMatrix = backEnd.shadowMap.modelviewMatrix.Multiply(entity->e.axis, entity->e.origin);

	// Set the modelview matrix
	GL_LoadMatrix(GL_MODELVIEW, backEnd.localParms.viewMatrix);

	// Set the depth range
	GL_DepthRange(0.0f, 1.0f);
}

/*
 ==================
 RB_TransformLightForEntity
 ==================
*/
void RB_TransformLightForEntity (light_t *light, entity_t *entity){

	// Transform light origin, light direction, and light axis into local space
	if (entity->e.model == NULL){
		backEnd.localParms.lightOrigin = light->data.origin;
		backEnd.localParms.lightDirection = light->data.direction;
		backEnd.localParms.lightAxis = light->l.axis;
	}
	else {
		R_WorldPointToLocal(light->data.origin, backEnd.localParms.lightOrigin, entity->e.origin, entity->e.axis);
		R_WorldVectorToLocal(light->data.direction, backEnd.localParms.lightDirection, entity->e.axis);
		R_WorldAxisToLocal(light->l.axis, backEnd.localParms.lightAxis, entity->e.axis);
	}

	// Compute the light plane
	if (entity->e.model == NULL){
		backEnd.localParms.lightPlane[0] = light->data.axis[0][0];
		backEnd.localParms.lightPlane[1] = light->data.axis[0][1];
		backEnd.localParms.lightPlane[2] = light->data.axis[0][2];
		backEnd.localParms.lightPlane[3] = -light->data.origin.Dot(light->data.axis[0]);
	}
	else {
		backEnd.localParms.lightPlane[0] = entity->e.axis[0].Dot(light->data.axis[0]);
		backEnd.localParms.lightPlane[1] = entity->e.axis[1].Dot(light->data.axis[0]);
		backEnd.localParms.lightPlane[2] = entity->e.axis[2].Dot(light->data.axis[0]);
		backEnd.localParms.lightPlane[3] = entity->e.origin.Dot(light->data.axis[0]) - light->data.origin.Dot(light->data.axis[0]);
	}
}

/*
 ==================
 RB_ComputeLightMatrix
 ==================
*/
void RB_ComputeLightMatrix (light_t *light, entity_t *entity, textureStage_t *textureStage){

	float	distanceScale, heightScale;

	if (backEnd.lightMaterial->lightType != LT_FOG){
		if (entity->e.model == NULL){
			if (!textureStage->numTexMods)
				backEnd.localParms.lightMatrix = light->data.modelviewProjectionMatrix;
			else {
				RB_ComputeTextureMatrix(textureStage, backEnd.lightRegisters, backEnd.localParms.lightMatrix);

				backEnd.localParms.lightMatrix *= light->data.modelviewProjectionMatrix;
			}
		}
		else {
			if (!textureStage->numTexMods)
				backEnd.localParms.lightMatrix = light->data.projectionMatrix * light->data.modelviewMatrix.Multiply(entity->e.axis, entity->e.origin);
			else {
				RB_ComputeTextureMatrix(textureStage, backEnd.lightRegisters, backEnd.localParms.lightMatrix);

				backEnd.localParms.lightMatrix *= light->data.projectionMatrix * light->data.modelviewMatrix.Multiply(entity->e.axis, entity->e.origin);
			}
		}

		return;
	}

	if (light->l.fogDistance < 1.0f)
		distanceScale = 0.5f / 500.0f;
	else
		distanceScale = 0.5f / light->l.fogDistance;

	if (light->l.fogHeight < 1.0f)
		heightScale = 0.5f / 500.0f;
	else
		heightScale = 0.5f / light->l.fogHeight;

	backEnd.localParms.lightMatrix[0][0] = backEnd.localParms.viewMatrix[0][0] * distanceScale;
	backEnd.localParms.lightMatrix[1][0] = backEnd.localParms.viewMatrix[1][0] * distanceScale;
	backEnd.localParms.lightMatrix[2][0] = backEnd.localParms.viewMatrix[2][0] * distanceScale;
	backEnd.localParms.lightMatrix[3][0] = backEnd.localParms.viewMatrix[3][0] * distanceScale + 0.5f;

	backEnd.localParms.lightMatrix[0][1] = backEnd.localParms.viewMatrix[0][2] * distanceScale;
	backEnd.localParms.lightMatrix[1][1] = backEnd.localParms.viewMatrix[1][2] * distanceScale;
	backEnd.localParms.lightMatrix[2][1] = backEnd.localParms.viewMatrix[2][2] * distanceScale;
	backEnd.localParms.lightMatrix[3][1] = backEnd.localParms.viewMatrix[3][2] * distanceScale + 0.5f;

	if (entity->e.model == NULL){
		backEnd.localParms.lightMatrix[0][2] = light->data.fogPlane.Normal()[0] * heightScale;
		backEnd.localParms.lightMatrix[1][2] = light->data.fogPlane.Normal()[1] * heightScale;
		backEnd.localParms.lightMatrix[2][2] = light->data.fogPlane.Normal()[2] * heightScale;
		backEnd.localParms.lightMatrix[3][2] = -light->data.fogPlane.Dist() * heightScale + 0.5f;
	}
	else {
		backEnd.localParms.lightMatrix[0][2] = entity->e.axis[0].Dot(light->data.fogPlane.Normal()) * heightScale;
		backEnd.localParms.lightMatrix[1][2] = entity->e.axis[1].Dot(light->data.fogPlane.Normal()) * heightScale;
		backEnd.localParms.lightMatrix[2][2] = entity->e.axis[2].Dot(light->data.fogPlane.Normal()) * heightScale;
		backEnd.localParms.lightMatrix[3][2] = (entity->e.origin.Dot(light->data.fogPlane.Normal()) - light->data.fogPlane.Dist()) * heightScale + 0.5f;
	}

	backEnd.localParms.lightMatrix[0][3] = 0.0f;
	backEnd.localParms.lightMatrix[1][3] = 0.0f;
	backEnd.localParms.lightMatrix[2][3] = 0.0f;
	backEnd.localParms.lightMatrix[3][3] = (backEnd.viewParms.origin.Dot(light->data.fogPlane.Normal()) - light->data.fogPlane.Dist()) * heightScale + 0.5f;
}

/*
 ==================
 RB_ComputeShadowMatrix
 ==================
*/
void RB_ComputeShadowMatrix (light_t *light, entity_t *entity){

	int		i;

	if (!backEnd.shadowMap.available)
		return;

	if (light->l.type == RL_POINT || light->l.type == RL_CUBIC){
		backEnd.localParms.shadowMatrix.Identity();
		backEnd.localParms.shadowMatrix.Translate(-light->data.origin[0], -light->data.origin[1], -light->data.origin[2]);

		if (entity->e.model == NULL)
			return;

		backEnd.localParms.shadowMatrix = backEnd.localParms.shadowMatrix.Multiply(entity->e.axis, entity->e.origin);

		return;
	}

	if (light->l.type == RL_PROJECTED){
		backEnd.localParms.shadowMatrix.Identity();
		backEnd.localParms.shadowMatrix.Translate(0.5f, 0.5f, 0.5f);
		backEnd.localParms.shadowMatrix.Scale(0.5f, 0.5f, 0.5f);

		if (entity->e.model == NULL)
			backEnd.localParms.shadowMatrix *= backEnd.shadowMap.modelviewProjectionMatrix;
		else
			backEnd.localParms.shadowMatrix *= backEnd.shadowMap.projectionMatrix * backEnd.shadowMap.modelviewMatrix.Multiply(entity->e.axis, entity->e.origin);

		return;
	}

	for (i = 0; i < backEnd.shadowMap.numSplits; i++){
		backEnd.localParms.shadowMatrixArray[i].Identity();
		backEnd.localParms.shadowMatrixArray[i].Translate(0.5f, 0.5f, 0.5f);
		backEnd.localParms.shadowMatrixArray[i].Scale(0.5f, 0.5f, 0.5f);

		if (entity->e.model == NULL)
			backEnd.localParms.shadowMatrixArray[i] *= backEnd.shadowMap.modelviewProjectionMatrixArray[i];
		else
			backEnd.localParms.shadowMatrixArray[i] *= backEnd.shadowMap.projectionMatrixArray[i] * backEnd.shadowMap.modelviewMatrixArray[i].Multiply(entity->e.axis, entity->e.origin);
	}
}

/*
 ==================
 RB_BindIndexBuffer
 ==================
*/
void RB_BindIndexBuffer (){

	bool	discard;

	if (backEnd.debugRendering){
		GL_BindIndexBuffer(NULL);
		return;
	}

	// If we already have an index buffer, bind it
	if (backEnd.indexBuffer){
		backEnd.indexPointer = NULL;

		GL_BindIndexBuffer(backEnd.indexBuffer);

		return;
	}

	// The current batch of indices is entirely dynamic, so copy it to a
	// dynamic index buffer if possible
	if (!backEnd.dynamicIndexBuffers[0] || !backEnd.dynamicIndexBuffers[1]){
		GL_BindIndexBuffer(NULL);
		return;
	}

	// In addition to double-buffering, we also discard the entire buffer each
	// time we swap index buffers to be able to write to it asynchronously
	if (backEnd.dynamicIndexOffset + backEnd.numIndices <= MAX_DYNAMIC_INDICES)
		discard = false;
	else {
		discard = true;

		backEnd.dynamicIndexOffset = 0;
		backEnd.dynamicIndexNumber ^= 1;
	}

	backEnd.indexBuffer = backEnd.dynamicIndexBuffers[backEnd.dynamicIndexNumber];
	backEnd.indexPointer = INDEX_OFFSET(NULL, backEnd.dynamicIndexOffset);

	// Bind the index buffer
	GL_BindIndexBuffer(backEnd.indexBuffer);

	// Upload the indices
	R_UpdateIndexBuffer(backEnd.indexBuffer, backEnd.dynamicIndexOffset, backEnd.numIndices, backEnd.indices, discard, false);

	backEnd.dynamicIndexOffset += backEnd.numIndices;
}

/*
 ==================
 RB_BindVertexBuffer
 ==================
*/
void RB_BindVertexBuffer (){

	bool	discard;

	if (backEnd.debugRendering){
		GL_BindVertexBuffer(NULL);
		return;
	}

	// If we already have a vertex buffer, bind it
	if (backEnd.vertexBuffer){
		backEnd.vertexPointer = NULL;

		GL_BindVertexBuffer(backEnd.vertexBuffer);

		return;
	}

	// The current batch of vertices is entirely dynamic, so copy it to a
	// dynamic vertex buffer if possible
	if (!backEnd.dynamicVertexBuffers[0] || !backEnd.dynamicVertexBuffers[1]){
		GL_BindVertexBuffer(NULL);
		return;
	}

	// In addition to double-buffering, we also discard the entire buffer each
	// time we swap vertex buffers to be able to write to it asynchronously
	if (backEnd.dynamicVertexOffset + backEnd.numVertices <= MAX_DYNAMIC_VERTICES)
		discard = false;
	else {
		discard = true;

		backEnd.dynamicVertexOffset = 0;
		backEnd.dynamicVertexNumber ^= 1;
	}

	backEnd.vertexBuffer = backEnd.dynamicVertexBuffers[backEnd.dynamicVertexNumber];
	backEnd.vertexPointer = VERTEX_OFFSET(NULL, backEnd.dynamicVertexOffset);

	// Bind the vertex buffer
	GL_BindVertexBuffer(backEnd.vertexBuffer);

	// Upload the vertices
	R_UpdateVertexBuffer(backEnd.vertexBuffer, backEnd.dynamicVertexOffset, backEnd.numVertices, backEnd.vertices, discard, false);

	backEnd.dynamicVertexOffset += backEnd.numVertices;
}

/*
 ==================
 RB_DrawElements
 ==================
*/
void RB_DrawElements (){

	if (r_skipDrawElements->integerValue)
		return;

	rg.pc.draws++;
	rg.pc.totalIndices += backEnd.numIndices;
	rg.pc.totalVertices += backEnd.numVertices;

	if (!backEnd.numDraws){
		qglDrawElements(GL_TRIANGLES, backEnd.numIndices, GL_INDEX_TYPE, backEnd.indexPointer);
		return;
	}

	qglMultiDrawElementsBaseVertex(GL_TRIANGLES, backEnd.drawIndexCount, GL_INDEX_TYPE, backEnd.drawIndexPointer, backEnd.numDraws, backEnd.drawBaseVertices);
}

/*
 ==================
 RB_DrawElementsWithCounters
 ==================
*/
void RB_DrawElementsWithCounters (int *totalIndices, int *totalVertices){

	if (r_skipDrawElements->integerValue)
		return;

	rg.pc.draws++;
	rg.pc.totalIndices += backEnd.numIndices;
	rg.pc.totalVertices += backEnd.numVertices;

	if (totalIndices)
		*totalIndices += backEnd.numIndices;
	if (totalVertices)
		*totalVertices += backEnd.numVertices;

	if (!backEnd.numDraws){
		qglDrawElements(GL_TRIANGLES, backEnd.numIndices, GL_INDEX_TYPE, backEnd.indexPointer);
		return;
	}

	qglMultiDrawElementsBaseVertex(GL_TRIANGLES, backEnd.drawIndexCount, GL_INDEX_TYPE, backEnd.drawIndexPointer, backEnd.numDraws, backEnd.drawBaseVertices);
}

/*
 ==================
 RB_DrawElementsStaticIndices
 ==================
*/
void RB_DrawElementsStaticIndices (int numVertices, int numIndices, const void *indices){

	if (r_skipDrawElements->integerValue)
		return;

	rg.pc.draws++;
	rg.pc.totalIndices += numIndices;
	rg.pc.totalVertices += numVertices;

	qglDrawElements(GL_TRIANGLES, numIndices, GL_INDEX_TYPE, indices);
}